{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "86706e34",
   "metadata": {},
   "source": [
    "### 一、数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ce75ecfc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Dataset: PubMed():\n",
      "==================\n",
      "Number of graphs: 1\n",
      "Number of features: 500\n",
      "Number of classes: 3\n",
      "\n",
      "Data(x=[19717, 500], edge_index=[2, 88648], y=[19717], train_mask=[19717], val_mask=[19717], test_mask=[19717])\n",
      "===============================================================================================================\n",
      "Number of nodes: 19717\n",
      "Number of edges: 88648\n",
      "Average node degree: 4.50\n",
      "Number of training nodes: 60\n",
      "Training node label rate: 0.003\n",
      "Has isolated nodes: False\n",
      "Has self-loops: False\n",
      "Is undirected: True\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "from torch_geometric.datasets import Planetoid\n",
    "from torch_geometric.transforms import NormalizeFeatures\n",
    "\n",
    "dataset = Planetoid(root='data/Planetoid', name='PubMed', transform=NormalizeFeatures())\n",
    "\n",
    "print(f'Dataset: {dataset}:')\n",
    "print('==================')\n",
    "print(f'Number of graphs: {len(dataset)}')\n",
    "print(f'Number of features: {dataset.num_features}')\n",
    "print(f'Number of classes: {dataset.num_classes}')\n",
    "\n",
    "data = dataset[0]  # 获取第一个图对象\n",
    "\n",
    "print()\n",
    "print(data)\n",
    "print('===============================================================================================================')\n",
    "\n",
    "# 获得这张图的一些统计信息\n",
    "print(f'Number of nodes: {data.num_nodes}')\n",
    "print(f'Number of edges: {data.num_edges}')\n",
    "print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')\n",
    "print(f'Number of training nodes: {data.train_mask.sum()}')\n",
    "print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.3f}')\n",
    "print(f'Has isolated nodes: {data.has_isolated_nodes()}')\n",
    "print(f'Has self-loops: {data.has_self_loops()}')\n",
    "print(f'Is undirected: {data.is_undirected()}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ac33bc59",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "60\n",
      "500\n",
      "1000\n"
     ]
    }
   ],
   "source": [
    "print(int(data.train_mask.sum())) # 训练集节点数\n",
    "print(int(data.val_mask.sum())) # 验证集节点数\n",
    "print(int(data.test_mask.sum())) # 测试集节点数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f74f3e05",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "类别 0: 4103 个样本\n",
      "类别 1: 7739 个样本\n",
      "类别 2: 7875 个样本\n"
     ]
    },
    {
     "data": {
      "image/png": "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/vjjqlixoiTplltu0eDBg832mjVr6h//+IfeeOMNc95HH32kZ555Rk888YQkqXbt2tqxY4def/11AhIkSbm5uZo7d6569+6tsmX/++e1Xr16qlOnjp555hlNnjxZlStX1tKlS5WQkKDly5e7rWPt2rVKSUlxOwOVZ968efLx8VGzZs0kSZ999pnmzJmjDz744PoO7Cbl0YA0fPhwjRo1St27d5ckNWrUSD///LMmTJig3r17m19qaWlpbgk5LS1NTZs2lSQ5HA6lp6e7rff8+fM6ceKEubzD4VBaWppbTd5nHpUEbix5TxANGzaswBdUJiUlKTk5ucBLG3mOHTumzz77zHxEW5KysrLk6+vrVufn56ft27crJyfniv63jxvb6tWrlZqaqr59+7rNL1eunL788kuNGjVKXbt21enTp1WnTh3NmzdP999/v1vthx9+qLZt26pevXoFbmP8+PH6+eefVbZsWdWrV08LFy7Uo48+et3GdFMr+luirlxgYKAxY8YMt3mvvfaacdtttxmG8d+btCdPnmy2Z2ZmFniT9s6dO82aVatWFXiTdnZ2tlkTFxd3yZu0rXiKDSg9CnqC6GKDBw826tevX2Bb9+7dDT8/P0OS0bVrV7enZePi4gyHw2Hs3LnTyM3NNXbs2GEEBwcbkoxjx45dl7EAuDal9im23r17G3/729/Mx/w/++wzo0qVKsaIESPMmokTJxoBAQHG559/buzZs8d46KGHCnzMv1mzZsa2bduMTZs2GbfddpvbY/4ZGRlGcHCw0bNnT2Pfvn3GJ598YpQvX57H/IEbUEFPEOX5448/DH9/f7f/dF3s+PHjxsGDB43PP//caNCggTF48GC3Zfv06WOULVvWKFOmjBESEmKMGDHCkGQ4nc7rMhYA1+Za/n57GcZFv8pYzE6dOqUXX3xRS5YsUXp6ukJCQtSjRw+NGTPGfOLMMAy99NJLmj17tjIyMnTXXXdpxowZ+vvf/26u58SJE4qNjdWyZcvk7e2tbt26adq0aeb9BdKfPxQZExOjHTt2qEqVKnr22Wc1cuTIK+qny+WSv7+/MjMzZbfbi3YnACgyP//8s2rXrq3PPvtMDz30UL72jz76SP369dOvv/6a774lq02bNunuu+/WsWPH3C7x5+TkmJf9Z8+erZEjRyojIyPfDbi4OrVGrfB0F+BhP02M+uui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b731lkaPHq2HHnpIkvTPf/5TwcHBWrp0qbp3766DBw9q5cqV2rFjh1q2bClJeuedd3T//fdr8uTJCgkJ0fz585Wdna05c+bIx8dHDRs2VHJyst588023IAUAACB5+AzSF198oZYtW+qxxx5TUFCQmjVrpvfff99sT0lJkdPpVEREhDnP399frVu3VmJioiQpMTFRAQEBZjiSpIiICHl7e2vbtm1mTbt27eTj42PWREZG6tChQzp58mS+fmVlZcnlcrlNAADg5uHRgPTjjz9q5syZuu2227Rq1SoNHjxYzz33nObNmydJcjqdkqTg4GC35YKDg802p9OpoKAgt/ayZcsqMDDQraagdVy8jYtNmDBB/v7+5hQaGloEowUAAKWFRwNSbm6umjdvrtdee03NmjXTwIEDNWDAAM2aNcuT3VJcXJwyMzPN6ejRox7tDwAAKF4eDUjVqlVTgwYN3ObVr19fqampkiSHwyFJSktLc6tJS0sz2xwOh9LT093az58/rxMnTrjVFLSOi7dxMZvNJrvd7jYBAICbh0cD0p133qlDhw65zfv+++9Vs2ZNSX/esO1wOLRmzRqz3eVyadu2bQoPD5ckhYeHKyMjQ0lJSWbN2rVrlZubq9atW5s1GzduVE5OjlmTkJCgunXruj0xBwAAIHk4IA0dOlRbt27Va6+9piNHjmjBggWaPXu2YmJiJEleXl4aMmSIXnnlFX3xxRfau3evevXqpZCQED388MOS/jzj1LlzZw0YMEDbt2/X5s2bFRsbq+7duyskJESS9OSTT8rHx0f9+vXT/v37tXDhQr399tsaNmyYp4YOAABKMI8+5n/HHXdoyZIliouL07hx4xQWFqa33npL0dHRZs2IESN05swZDRw4UBkZGbrrrru0cuVK+fr6mjXz589XbGys7r33Xnl7e6tbt26aNm2a2e7v76+vv/5aMTExatGihapUqaIxY8bwiD8AACiQl2EYhqc7UdK5XC75+/srMzOT+5FwQ6o1aoWnuwAP+2lilEe3zzGI63EMXsvfb4+/agQAAKCkISABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABYEJAAAAAsCEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFh4NCCNHTtWXl5eblO9evXM9nPnzikmJkaVK1dWxYoV1a1bN6WlpbmtIzU1VVFRUSpfvryCgoI0fPhwnT9/3q1m/fr1at68uWw2m+rUqaP4+PjiGB4AACilPH4GqWHDhjp+/Lg5bdq0yWwbOnSoli1bpsWLF2vDhg06duyYHnnkEbP9woULioqKUnZ2trZs2aJ58+YpPj5eY8aMMWtSUlIUFRWljh07Kjk5WUOGDFH//v21atWqYh0nAAAoPcp6vANly8rhcOSbn5mZqQ8//FALFizQPffcI0maO3eu6tevr61bt6pNmzb6+uuvdeDAAa1evVrBwcFq2rSpxo8fr5EjR2rs2LHy8fHRrFmzFBYWpilTpkiS6tevr02bNmnq1KmKjIwssE9ZWVnKysoyP7tcruswcgAAUFJ5/AzS4cOHFRISotq1ays6OlqpqamSpKSkJOXk5CgiIsKsrVevnmrUqKHExERJUmJioho1aqTg4GCzJjIyUi6XS/v37zdrLl5HXk3eOgoyYcIE+fv7m1NoaGiRjRcAAJR8Hg1IrVu3Vnx8vFauXKmZM2cqJSVFd999t06dOiWn0ykfHx8FBAS4LRMcHCyn0ylJcjqdbuEorz2v7XI1LpdLZ8+eLbBfcXFxyszMNKejR48WxXABAEAp4dFLbF26dDH/3bhxY7Vu3Vo1a9bUokWL5Ofn57F+2Ww22Ww2j20fAAB4lscvsV0sICBAf//733XkyBE5HA5lZ2crIyPDrSYtLc28Z8nhcOR7qi3v81/V2O12j4YwAABQcpWogHT69Gn98MMPqlatmlq0aKFy5cppzZo1ZvuhQ4eUmpqq8PBwSVJ4eLj27t2r9PR0syYhIUF2u10NGjQway5eR15N3joAAACsPBqQ/t//+3/asGGDfvrpJ23ZskX/8z//ozJlyqhHjx7y9/dXv379NGzYMK1bt05JSUnq06ePwsPD1aZNG0lSp06d1KBBA/Xs2VO7d+/WqlWrNHr0aMXExJiXyAYNGqQff/xRI0aM0HfffacZM2Zo0aJFGjp0qCeHDgAASjCP3oP0yy+/qEePHvr9999VtWpV3XXXXdq6dauqVq0qSZo6daq8vb3VrVs3ZWVlKTIyUjNmzDCXL1OmjJYvX67BgwcrPDxcFSpUUO/evTVu3DizJiwsTCtWrNDQoUP19ttvq3r16vrggw8u+Yg/AACAl2EYhqc7UdK5XC75+/srMzNTdrvd090BilytUSs83QV42E8Tozy6fY5BXI9j8Fr+fpeoe5AAAABKAgISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCiUAHp6NGj+uWXX8zP27dv15AhQzR79uwi6xgAAICnFCogPfnkk1q3bp0kyel06r777tP27dv1v//7vxo3blyRdhAAAKC4FSog7du3T61atZIkLVq0SLfffru2bNmi+fPnKz4+vij7BwAAUOwKFZBycnJks9kkSatXr9aDDz4oSapXr56OHz9edL0DAADwgEIFpIYNG2rWrFn65ptvlJCQoM6dO0uSjh07psqVKxdpBwEAAIpboQLS66+/rvfee08dOnRQjx491KRJE0nSF198YV56AwAAKK3KFmahDh066D//+Y9cLpduueUWc/7AgQNVoUKFIuscAACAJxTqDNI999yjU6dOuYUjSQoMDNQTTzxRJB0DAADwlEIFpPXr1ys7Ozvf/HPnzumbb7655k4BAAB40lUFpD179mjPnj2SpAMHDpif9+zZo127dunDDz/U3/72t+vSUZQOEydOlJeXl4YMGWLOmz17tjp06CC73S4vLy9lZGTkW+7EiROKjo6W3W5XQECA+vXrp9OnT5vthw4dUseOHRUcHCxfX1/Vrl1bo0ePVk5OTjGMCgBws7mqe5CaNm0qLy8veXl56Z577snX7ufnp3feeafIOofSZceOHXrvvffUuHFjt/l//PGHOnfurM6dOysuLq7AZaOjo3X8+HElJCQoJydHffr00cCBA7VgwQJJUrly5dSrVy81b95cAQEB2r17twYMGKDc3Fy99tpr131sAICby1UFpJSUFBmGodq1a2v79u2qWrWq2ebj46OgoCCVKVOmyDuJku/06dOKjo7W+++/r1deecWtLe9s0vr16wtc9uDBg1q5cqV27Nihli1bSpLeeecd3X///Zo8ebJCQkJUu3Zt1a5d21ymZs2aWr9+PZd0AQDXxVVdYqtZs6Zq1aql3NxctWzZUjVr1jSn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2sbcuXOLvd8lxdXuv9dff9249dZbDV9fXyMwMNDo0KGDsXbtWs90voQoaP9Jcjuu+C68tMLsv6L4HvT6v40DAADg/3APEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISgFKpQ4cOGjJkyBXVrl+/Xl5eXsrIyLimbdaqVUtvvfXWNa0DQOlAQAIAALAgIAEAAFgQkACUeh999JFatmypSpUqyeFw6Mknn1R6enq+us2bN6tx48by9fVVmzZttG/fPrf2TZs26e6775afn59CQ0P13HPP6cyZMwVu0zAMjR07VjVq1JDNZlNISIiee+656zI+AMWPgASg1MvJydH48eO1e/duLV26VD/99JOefvrpfHXDhw/XlClTtGPHDlWtWlVdu3ZVTk6OJOmHH35Q586d1a1bN+3Zs0cLFy7Upk2bFBsbW+A2P/30U02dOlXvvfeeDh8+rKVLl6pRo0bXc5gAilFZT3cAAK5V3759zX/Xrl1b06ZN0x133KHTp0+rYsWKZttLL72k++67T5I0b948Va9eXUuWLNHjjz+uCRMmKDo62rzx+7bbbtO0adPUvn17zZw5U76+vm7bTE1NlcPhUEREhMqVK6caNWqoVatW13+wAIoFZ5AAlHpJSUnq2rWratSooUqVKql9+/aS/gwxFwsPDzf/HRgYqLp16+rgwYOSpN27dys+Pl4VK1Y0p8jISOXm5iolJSXfNh977DGdPXtWtWvX1oABA7RkyRKdP3/+Oo4SQHEiIAEo1c6cOaPIyEjZ7XbNnz9fO3bs0JIlSyRJ2dnZV7ye06dP65lnnlFycrI57d69W4cPH9att96arz40NFSHDh3SjBkz5Ofnp3/84x9q166deckOQOnGJTYApdp3332n33//XRMnTlRoaKgkaefOnQXWbt26VTVq1JAknTx5Ut9//73q168vSWrevLkOHDigOnXqXPG2/fz81LVrV3Xt2lUxMTGqV6+e9u7dq+bNm1/jqAB4GgEJQKlWo0YN+fj46J133tGgQYO0b98+jR8/vsDacePGqXLlygoODtb//u//qkqVKnr44YclSSNHjlSbNm0UGxur/v37q0KFCjpw4IASEhL07rvv5ltXfHy8Lly4oNatW6t8+fL6+OOP5efnp5o1a17P4QIoJlxiA1CqVa1aVfHx8Vq8eLEaNGigiRMnavLkyQXWTpw4Uc8//7xatGghp9OpZcuWycfHR5LUuHFjbdiwQd9//73uvvtuNWvWTGPGjFFISEiB6woICND777+vO++8U40bN9bq1au1bNkyVa5c+bqNFUDx8TIMw/B0JwAAAEoSziABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABb/H5Mguu5z4lazAAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 统计每个类别的数量\n",
    "unique_labels, counts = torch.unique(data.y, return_counts=True)\n",
    "\n",
    "# 打印每个类别的数量\n",
    "for label, count in zip(unique_labels, counts):\n",
    "    print(f\"类别 {label.item()}: {count.item()} 个样本\")\n",
    "\n",
    "# 使用直方图可视化\n",
    "plt.bar(unique_labels.numpy(), counts.numpy())\n",
    "plt.xlabel('labels')\n",
    "plt.ylabel('counts')\n",
    "\n",
    "# 在每个条形上方添加数字标签\n",
    "for label, count in zip(unique_labels, counts):\n",
    "    plt.text(label.item(), count.item(), str(count.item()), ha='center', va='bottom')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03ccd99c",
   "metadata": {},
   "source": [
    "### 二、Full-batch加载数据训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ab05b903",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train(): \n",
    "    model.train() # 训练模式\n",
    "    optimizer.zero_grad()  # 梯度清0\n",
    "    train_outputs = model(data.x, data.edge_index)  # 前向传播\n",
    "    loss = criterion(train_outputs[data.train_mask], data.y[data.train_mask])  # 计算损失\n",
    "    loss.backward()  # 反向传播\n",
    "    optimizer.step()  # 参数更新\n",
    "    \n",
    "    return loss\n",
    "    \n",
    "def test():    \n",
    "    model.eval() # 测试模式\n",
    "    outputs = model(data.x, data.edge_index)  # 预测输出\n",
    "    preds = outputs.argmax(dim=1) # 测试预测\n",
    "    \n",
    "    accs = []\n",
    "    for mask in [data.train_mask, data.val_mask, data.test_mask]:\n",
    "        correct = preds[mask] == data.y[mask]  # 预测正确\n",
    "        accs.append(int(correct.sum()) / int(mask.sum()))  # 准确率\n",
    "    \n",
    "    return accs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5fd464c8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GCN(\n",
      "  (conv1): GCNConv(500, 32)\n",
      "  (conv2): GCNConv(32, 3)\n",
      "  (dp): Dropout(p=0.5, inplace=False)\n",
      ")\n",
      "Total parameters: 16131\n"
     ]
    }
   ],
   "source": [
    "import torch.nn as nn\n",
    "from torch_geometric.nn import GCNConv\n",
    "\n",
    "class GCN(nn.Module):\n",
    "    def __init__(self, output_channels=3):\n",
    "        super(GCN, self).__init__()\n",
    "\n",
    "        self.conv1 = GCNConv(500, 32)  \n",
    "        self.conv2 = GCNConv(32, output_channels)\n",
    "        \n",
    "        self.dp = nn.Dropout(p=0.5)\n",
    "\n",
    "    def forward(self, x, edge_index):\n",
    "        \n",
    "        x = F.relu(self.conv1(x, edge_index)) # (num_nodes, num_features=500) ——> (num_nodes, num_features=32)\n",
    "        x = self.conv2(x, edge_index) # (num_nodes, num_features=32) ——> (num_nodes, dataset.num_classes=3)\n",
    "        \n",
    "        return x\n",
    "\n",
    "model = GCN()\n",
    "print(model)\n",
    "\n",
    "# 统计模型总参数量\n",
    "total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "print(f\"Total parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "11432b73",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch #000, Loss: 1.0993, Train_Accuracy: 0.3500, Val_Accuracy: 0.1980, Test_Accuracy: 0.1860\n",
      "Epoch #010, Loss: 0.9227, Train_Accuracy: 0.9333, Val_Accuracy: 0.7180, Test_Accuracy: 0.6790\n",
      "Epoch #020, Loss: 0.6597, Train_Accuracy: 0.9500, Val_Accuracy: 0.7500, Test_Accuracy: 0.7280\n",
      "Epoch #030, Loss: 0.4127, Train_Accuracy: 0.9500, Val_Accuracy: 0.7660, Test_Accuracy: 0.7480\n",
      "Epoch #040, Loss: 0.2422, Train_Accuracy: 0.9667, Val_Accuracy: 0.7840, Test_Accuracy: 0.7650\n",
      "Epoch #050, Loss: 0.1456, Train_Accuracy: 1.0000, Val_Accuracy: 0.7920, Test_Accuracy: 0.7650\n",
      "Epoch #060, Loss: 0.0962, Train_Accuracy: 1.0000, Val_Accuracy: 0.7880, Test_Accuracy: 0.7700\n",
      "Epoch #070, Loss: 0.0718, Train_Accuracy: 1.0000, Val_Accuracy: 0.7800, Test_Accuracy: 0.7730\n",
      "Epoch #080, Loss: 0.0597, Train_Accuracy: 1.0000, Val_Accuracy: 0.7740, Test_Accuracy: 0.7770\n",
      "Epoch #090, Loss: 0.0531, Train_Accuracy: 1.0000, Val_Accuracy: 0.7760, Test_Accuracy: 0.7820\n",
      "Epoch #100, Loss: 0.0489, Train_Accuracy: 1.0000, Val_Accuracy: 0.7780, Test_Accuracy: 0.7850\n",
      "Elapsed time: 2.46929931640625 seconds\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\") # 使用GPU or CPU\n",
    "\n",
    "model = model.to(device) # 加载模型\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4) # 优化器\n",
    "criterion = nn.CrossEntropyLoss(reduction='mean')  # 损失函数\n",
    "\n",
    "data = data.to(device)\n",
    "\n",
    "start_time = time.time()\n",
    "\n",
    "for epoch in range(101):\n",
    "    loss = train()\n",
    "    train_acc, val_acc, test_acc = test()\n",
    "\n",
    "    if epoch % 10 == 0:\n",
    "        print('Epoch #{:03d}, Loss: {:.4f}, Train_Accuracy: {:.4f}, Val_Accuracy: {:.4f}, Test_Accuracy: {:.4f}'\n",
    "              .format(epoch, loss, train_acc, val_acc, test_acc))\n",
    "        \n",
    "end_time = time.time()\n",
    "elapsed_time = end_time - start_time\n",
    "print(f\"Elapsed time: {elapsed_time} seconds\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8622eacb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ClusterGCN(\n",
      "  (conv1): ClusterGCNConv(500, 32, diag_lambda=1)\n",
      "  (conv2): ClusterGCNConv(32, 3, diag_lambda=1)\n",
      "  (dp): Dropout(p=0.5, inplace=False)\n",
      ")\n",
      "Total parameters: 32227\n"
     ]
    }
   ],
   "source": [
    "import torch.nn as nn\n",
    "from torch_geometric.nn import ClusterGCNConv\n",
    "\n",
    "class ClusterGCN(nn.Module):\n",
    "    def __init__(self, output_channels=3):\n",
    "        super(ClusterGCN, self).__init__()\n",
    "\n",
    "        self.conv1 = ClusterGCNConv(500, 32, diag_lambda=1)  \n",
    "        self.conv2 = ClusterGCNConv(32, output_channels, diag_lambda=1)\n",
    "        \n",
    "        self.dp = nn.Dropout(p=0.5)\n",
    "\n",
    "    def forward(self, x, edge_index):\n",
    "        \n",
    "        x = F.relu(self.conv1(x, edge_index)) # (num_nodes, num_features=500) ——> (num_nodes, num_features=32)\n",
    "        x = self.conv2(x, edge_index) # (num_nodes, num_features=32) ——> (num_nodes, dataset.num_classes=3)\n",
    "        \n",
    "        return x\n",
    "\n",
    "model = ClusterGCN()\n",
    "print(model)\n",
    "\n",
    "# 统计模型总参数量\n",
    "total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "print(f\"Total parameters: {total_params}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "6ad662ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 重置先前训练的模型权重\n",
    "for layer in model.children():\n",
    "    if hasattr(layer, 'reset_parameters'):\n",
    "        layer.reset_parameters()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "d6a61e2d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch #000, Loss: 1.1091, Train_Accuracy: 0.4833, Val_Accuracy: 0.4580, Test_Accuracy: 0.4500\n",
      "Epoch #010, Loss: 0.4512, Train_Accuracy: 0.9833, Val_Accuracy: 0.7580, Test_Accuracy: 0.7480\n",
      "Epoch #020, Loss: 0.0923, Train_Accuracy: 0.9833, Val_Accuracy: 0.7520, Test_Accuracy: 0.7580\n",
      "Epoch #030, Loss: 0.0215, Train_Accuracy: 1.0000, Val_Accuracy: 0.7560, Test_Accuracy: 0.7530\n",
      "Epoch #040, Loss: 0.0087, Train_Accuracy: 1.0000, Val_Accuracy: 0.7660, Test_Accuracy: 0.7540\n",
      "Epoch #050, Loss: 0.0061, Train_Accuracy: 1.0000, Val_Accuracy: 0.7620, Test_Accuracy: 0.7540\n",
      "Epoch #060, Loss: 0.0060, Train_Accuracy: 1.0000, Val_Accuracy: 0.7580, Test_Accuracy: 0.7560\n",
      "Epoch #070, Loss: 0.0065, Train_Accuracy: 1.0000, Val_Accuracy: 0.7600, Test_Accuracy: 0.7620\n",
      "Epoch #080, Loss: 0.0068, Train_Accuracy: 1.0000, Val_Accuracy: 0.7660, Test_Accuracy: 0.7620\n",
      "Epoch #090, Loss: 0.0067, Train_Accuracy: 1.0000, Val_Accuracy: 0.7720, Test_Accuracy: 0.7650\n",
      "Epoch #100, Loss: 0.0064, Train_Accuracy: 1.0000, Val_Accuracy: 0.7760, Test_Accuracy: 0.7680\n",
      "Elapsed time: 2.206155776977539 seconds\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\") # 使用GPU or CPU\n",
    "\n",
    "model = model.to(device) # 加载模型\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4) # 优化器\n",
    "criterion = nn.CrossEntropyLoss(reduction='mean')  # 损失函数\n",
    "\n",
    "data = data.to(device)\n",
    "\n",
    "start_time = time.time()\n",
    "\n",
    "for epoch in range(101):\n",
    "    loss = train()\n",
    "    train_acc, val_acc, test_acc = test()\n",
    "\n",
    "    if epoch % 10 == 0:\n",
    "        print('Epoch #{:03d}, Loss: {:.4f}, Train_Accuracy: {:.4f}, Val_Accuracy: {:.4f}, Test_Accuracy: {:.4f}'\n",
    "              .format(epoch, loss, train_acc, val_acc, test_acc))\n",
    "        \n",
    "end_time = time.time()\n",
    "elapsed_time = end_time - start_time\n",
    "print(f\"Elapsed time: {elapsed_time} seconds\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9a5f8bc",
   "metadata": {},
   "source": [
    "### 二、ClusterLoader加载数据训练"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "31aa052f",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Computing METIS partitioning...\n",
      "Done!\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Step 1:\n",
      "=======\n",
      "Number of nodes in the current batch: 4935\n",
      "Data(x=[4935, 500], y=[4935], train_mask=[4935], val_mask=[4935], test_mask=[4935], edge_index=[2, 17064])\n",
      "\n",
      "Step 2:\n",
      "=======\n",
      "Number of nodes in the current batch: 4957\n",
      "Data(x=[4957, 500], y=[4957], train_mask=[4957], val_mask=[4957], test_mask=[4957], edge_index=[2, 19358])\n",
      "\n",
      "Step 3:\n",
      "=======\n",
      "Number of nodes in the current batch: 4924\n",
      "Data(x=[4924, 500], y=[4924], train_mask=[4924], val_mask=[4924], test_mask=[4924], edge_index=[2, 15304])\n",
      "\n",
      "Step 4:\n",
      "=======\n",
      "Number of nodes in the current batch: 4901\n",
      "Data(x=[4901, 500], y=[4901], train_mask=[4901], val_mask=[4901], test_mask=[4901], edge_index=[2, 15634])\n",
      "\n",
      "Iterated over 19717 of 19717 nodes!\n"
     ]
    }
   ],
   "source": [
    "from torch_geometric.loader import ClusterData, ClusterLoader\n",
    "\n",
    "torch.manual_seed(16)\n",
    "\n",
    "data = data.cpu()\n",
    "\n",
    "cluster_data = ClusterData(data, num_parts=128)  # 1.创建子图\n",
    "train_loader = ClusterLoader(cluster_data, batch_size=32, shuffle=True)  # 2.随机分区\n",
    "\n",
    "total_num_nodes = 0\n",
    "for step, sub_data in enumerate(train_loader):\n",
    "    print(f'Step {step + 1}:')\n",
    "    print('=======')\n",
    "    print(f'Number of nodes in the current batch: {sub_data.num_nodes}')\n",
    "    print(sub_data)\n",
    "    print()\n",
    "    total_num_nodes += sub_data.num_nodes\n",
    "\n",
    "print(f'Iterated over {total_num_nodes} of {data.num_nodes} nodes!')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "c3786bf1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train(): \n",
    "    model.train() # 训练模式\n",
    "    \n",
    "    loss_epoch = []\n",
    "    \n",
    "    # 每个Mini-batch更新一次参数\n",
    "    for sub_data in train_loader:\n",
    "        sub_data.to(device)\n",
    "        optimizer.zero_grad()  # 梯度清0\n",
    "        train_outputs = model(sub_data.x, sub_data.edge_index)  # 前向传播\n",
    "        loss = criterion(train_outputs[sub_data.train_mask], sub_data.y[sub_data.train_mask])  # 计算损失\n",
    "        loss.backward()  # 反向传播\n",
    "        optimizer.step()  # 参数更新\n",
    "        \n",
    "        loss_epoch.append(round(float(loss.cpu()), 4))\n",
    "    \n",
    "    return loss_epoch\n",
    "    \n",
    "def test():    \n",
    "    model.eval() # 测试模式\n",
    "    outputs = model(data.x, data.edge_index)  # 预测输出\n",
    "    preds = outputs.argmax(dim=1) # 测试预测\n",
    "    \n",
    "    accs = []\n",
    "    for mask in [data.train_mask, data.val_mask, data.test_mask]:\n",
    "        correct = preds[mask] == data.y[mask]  # 预测正确\n",
    "        accs.append(int(correct.sum()) / int(mask.sum()))  # 准确率\n",
    "    \n",
    "    return accs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "03b6e5cc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "GCN(\n",
      "  (conv1): GCNConv(500, 32)\n",
      "  (conv2): GCNConv(32, 3)\n",
      "  (dp): Dropout(p=0.5, inplace=False)\n",
      ")\n",
      "Total parameters: 16131\n"
     ]
    }
   ],
   "source": [
    "model = GCN()\n",
    "print(model)\n",
    "\n",
    "# 统计模型总参数量\n",
    "total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "print(f\"Total parameters: {total_params}\")\n",
    "\n",
    "# 重置先前训练的模型权重\n",
    "for layer in model.children():\n",
    "    if hasattr(layer, 'reset_parameters'):\n",
    "        layer.reset_parameters()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "bc231a6c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch #000, Loss: [1.1001, 1.0836, 1.1015, 1.1077], Train_Accuracy: 0.3333, Val_Accuracy: 0.3880, Test_Accuracy: 0.4130\n",
      "Epoch #010, Loss: [0.5660, 0.4960, 0.5846, 0.4397], Train_Accuracy: 0.9500, Val_Accuracy: 0.7660, Test_Accuracy: 0.7620\n",
      "Epoch #020, Loss: [0.1224, 0.1632, 0.1140, 0.2069], Train_Accuracy: 0.9833, Val_Accuracy: 0.7940, Test_Accuracy: 0.7800\n",
      "Epoch #030, Loss: [0.0835, 0.0817, 0.0606, 0.0455], Train_Accuracy: 1.0000, Val_Accuracy: 0.8020, Test_Accuracy: 0.7850\n",
      "Epoch #040, Loss: [0.0464, 0.0545, 0.0518, 0.0239], Train_Accuracy: 1.0000, Val_Accuracy: 0.8000, Test_Accuracy: 0.7820\n",
      "Epoch #050, Loss: [0.0386, 0.0398, 0.0353, 0.0395], Train_Accuracy: 1.0000, Val_Accuracy: 0.7940, Test_Accuracy: 0.7790\n",
      "Epoch #060, Loss: [0.0372, 0.0301, 0.0419, 0.0209], Train_Accuracy: 1.0000, Val_Accuracy: 0.8020, Test_Accuracy: 0.7830\n",
      "Epoch #070, Loss: [0.0311, 0.0258, 0.0300, 0.0204], Train_Accuracy: 1.0000, Val_Accuracy: 0.8000, Test_Accuracy: 0.7890\n",
      "Epoch #080, Loss: [0.0220, 0.0323, 0.0224, 0.0296], Train_Accuracy: 1.0000, Val_Accuracy: 0.8000, Test_Accuracy: 0.7890\n",
      "Epoch #090, Loss: [0.0276, 0.0203, 0.0301, 0.0174], Train_Accuracy: 1.0000, Val_Accuracy: 0.8040, Test_Accuracy: 0.7890\n",
      "Epoch #100, Loss: [0.0212, 0.0237, 0.0197, 0.0192], Train_Accuracy: 1.0000, Val_Accuracy: 0.8020, Test_Accuracy: 0.7900\n",
      "Elapsed time: 6.870549917221069 seconds\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\") # 使用GPU or CPU\n",
    "\n",
    "model = model.to(device) # 加载模型\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4) # 优化器\n",
    "criterion = nn.CrossEntropyLoss(reduction='mean')  # 损失函数\n",
    "\n",
    "data = data.to(device)\n",
    "\n",
    "start_time = time.time()\n",
    "\n",
    "for epoch in range(101):\n",
    "    loss = train()\n",
    "    # 将 loss 列表中的所有元素转换为字符串，并以逗号分隔拼接\n",
    "    loss_str = ', '.join(['{:.4f}'.format(i) for i in loss])\n",
    "    \n",
    "    train_acc, val_acc, test_acc = test()\n",
    "\n",
    "    if epoch % 10 == 0:\n",
    "        print('Epoch #{:03d}, Loss: [{}], Train_Accuracy: {:.4f}, Val_Accuracy: {:.4f}, Test_Accuracy: {:.4f}'\n",
    "              .format(epoch, loss_str, train_acc, val_acc, test_acc))\n",
    "        \n",
    "end_time = time.time()\n",
    "elapsed_time = end_time - start_time\n",
    "print(f\"Elapsed time: {elapsed_time} seconds\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "6696efcf",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ClusterGCN(\n",
      "  (conv1): ClusterGCNConv(500, 32, diag_lambda=1)\n",
      "  (conv2): ClusterGCNConv(32, 3, diag_lambda=1)\n",
      "  (dp): Dropout(p=0.5, inplace=False)\n",
      ")\n",
      "Total parameters: 32227\n"
     ]
    }
   ],
   "source": [
    "model = ClusterGCN()\n",
    "print(model)\n",
    "\n",
    "# 统计模型总参数量\n",
    "total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "print(f\"Total parameters: {total_params}\")\n",
    "\n",
    "# 重置先前训练的模型权重\n",
    "for layer in model.children():\n",
    "    if hasattr(layer, 'reset_parameters'):\n",
    "        layer.reset_parameters()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "142a1239",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch #000, Loss: [1.0764, 1.1757, 1.0424, 1.0692], Train_Accuracy: 0.7333, Val_Accuracy: 0.6440, Test_Accuracy: 0.6600\n",
      "Epoch #010, Loss: [0.0153, 0.0583, 0.0415, 0.0211], Train_Accuracy: 1.0000, Val_Accuracy: 0.7460, Test_Accuracy: 0.7570\n",
      "Epoch #020, Loss: [0.0088, 0.0092, 0.0054, 0.0115], Train_Accuracy: 1.0000, Val_Accuracy: 0.7700, Test_Accuracy: 0.7610\n",
      "Epoch #030, Loss: [0.0040, 0.0074, 0.0051, 0.0095], Train_Accuracy: 1.0000, Val_Accuracy: 0.7760, Test_Accuracy: 0.7670\n",
      "Epoch #040, Loss: [0.0070, 0.0050, 0.0045, 0.0054], Train_Accuracy: 1.0000, Val_Accuracy: 0.7760, Test_Accuracy: 0.7680\n",
      "Epoch #050, Loss: [0.0034, 0.0060, 0.0050, 0.0044], Train_Accuracy: 1.0000, Val_Accuracy: 0.7880, Test_Accuracy: 0.7740\n",
      "Epoch #060, Loss: [0.0054, 0.0052, 0.0046, 0.0046], Train_Accuracy: 1.0000, Val_Accuracy: 0.7860, Test_Accuracy: 0.7790\n",
      "Epoch #070, Loss: [0.0037, 0.0043, 0.0047, 0.0038], Train_Accuracy: 1.0000, Val_Accuracy: 0.7880, Test_Accuracy: 0.7700\n",
      "Epoch #080, Loss: [0.0045, 0.0035, 0.0041, 0.0035], Train_Accuracy: 1.0000, Val_Accuracy: 0.7840, Test_Accuracy: 0.7680\n",
      "Epoch #090, Loss: [0.0035, 0.0033, 0.0025, 0.0061], Train_Accuracy: 1.0000, Val_Accuracy: 0.7780, Test_Accuracy: 0.7730\n",
      "Epoch #100, Loss: [0.0047, 0.0023, 0.0029, 0.0054], Train_Accuracy: 1.0000, Val_Accuracy: 0.7880, Test_Accuracy: 0.7690\n",
      "Elapsed time: 8.59732174873352 seconds\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "\n",
    "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\") # 使用GPU or CPU\n",
    "\n",
    "model = model.to(device) # 加载模型\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4) # 优化器\n",
    "criterion = nn.CrossEntropyLoss(reduction='mean')  # 损失函数\n",
    "\n",
    "data = data.to(device)\n",
    "\n",
    "start_time = time.time()\n",
    "\n",
    "for epoch in range(101):\n",
    "    loss = train()\n",
    "    # 将 loss 列表中的所有元素转换为字符串，并以逗号分隔拼接\n",
    "    loss_str = ', '.join(['{:.4f}'.format(i) for i in loss])\n",
    "    \n",
    "    train_acc, val_acc, test_acc = test()\n",
    "\n",
    "    if epoch % 10 == 0:\n",
    "        print('Epoch #{:03d}, Loss: [{}], Train_Accuracy: {:.4f}, Val_Accuracy: {:.4f}, Test_Accuracy: {:.4f}'\n",
    "              .format(epoch, loss_str, train_acc, val_acc, test_acc))\n",
    "        \n",
    "end_time = time.time()\n",
    "elapsed_time = end_time - start_time\n",
    "print(f\"Elapsed time: {elapsed_time} seconds\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "deep_learning",
   "language": "python",
   "name": "deep_learning"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
